/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2010  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

/* This file is built into a shared object which is loaded dynamically
 * as a plugin into pppd.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <syslog.h>
#include <fcntl.h>
#include <pppd/pppd.h>
#include <pppd/fsm.h>
#include <pppd/ipcp.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <dbus/dbus.h>

#define INET_ADDRES_LEN (INET_ADDRSTRLEN + 5)
#define INET_DNS_LEN	(2*INET_ADDRSTRLEN + 9)

static char *busname = NULL;
static char *interface = NULL;
static char *path = NULL;
static const char syslog_prefix[] = "libppp-plugin.so";

static DBusConnection *connection = NULL;

char pppd_version[] = VERSION;

int plugin_init(void);

static void append(DBusMessageIter *dict, const char *key, const char *value)
{
	DBusMessageIter entry;
	/* We clean the environment before invoking openconnect, but
	   might as well still filter out the few things that get
	   added that we're not interested in */
	if (!strcmp(key, "PWD") || !strcmp(key, "_") ||
	    !strcmp(key, "SHLVL") || !strcmp(key, "connman_busname") ||
	    !strcmp(key, "connman_network"))
		return;

	dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY,
							NULL, &entry);

	dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key);

	dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &value);

	dbus_message_iter_close_container(dict, &entry);
}

static const char *get_phase_name(int phase) {
       static const char *names[] = {
		"DEAD",
		"INITIALIZE",
		"SERIALCONN",
		"DORMANT",
		"ESTABLISH",
		"AUTHENTICATE",
		"CALLBACK",
		"NETWORK",
		"RUNNING",
		"TERMINATE",
		"DISCONNECT",
		"HOLDOFF",
		"MASTER"
	};
	if (phase < 0 || phase >= sizeof(names)/sizeof(names[0])) {
		return "PHASE_UNKNOWN";
	}
	return names[phase];
}

static int pptp_have_secret()
{
	return 1;
}

static int pptp_get_secret(char *username, char *password)
{
	DBusMessage *msg, *reply;
	const char *user, *pass;
	DBusError err;

	if (username == NULL && password == NULL) {
		syslog(LOG_ERR, "%s: %s: username/password set to NULL",
				syslog_prefix, __func__);
		return -1;
	}

	if (password == NULL) {
		syslog(LOG_ERR, "%s: %s: password set to NULL",
				syslog_prefix, __func__);
		return 1;
	}
	if (connection == NULL) {
		syslog(LOG_ERR, "%s: %s: connection not set",
				syslog_prefix, __func__);
		return -1;
	}

	dbus_error_init(&err);

	msg = dbus_message_new_method_call(busname, path,
						interface, "getsec");
	if (msg == NULL) {
		syslog(LOG_ERR, "%s: %s: unable to create dbus call",
				syslog_prefix, __func__);
		return -1;
	}

	dbus_message_append_args(msg, DBUS_TYPE_INVALID, DBUS_TYPE_INVALID);

	reply = dbus_connection_send_with_reply_and_block(connection,
								msg, -1, &err);

	if (reply == NULL) {
		syslog(LOG_ERR, "%s: %s: unable to get dbus reply",
				syslog_prefix, __func__);
		if (dbus_error_is_set(&err) == TRUE)
			dbus_error_free(&err);

		dbus_message_unref(msg);
		return -1;
	}

	dbus_message_unref(msg);

	dbus_error_init(&err);

	if (dbus_message_get_args(reply, &err, DBUS_TYPE_STRING, &user,
						DBUS_TYPE_STRING, &pass,
						DBUS_TYPE_INVALID) == FALSE) {
		syslog(LOG_ERR, "%s: %s: unable to get args",
				syslog_prefix, __func__);
		if (dbus_error_is_set(&err) == TRUE)
			dbus_error_free(&err);

		dbus_message_unref(reply);
		return -1;
	}

	if (username != NULL)
		strcpy(username, user);

	strcpy(password, pass);

	dbus_message_unref(reply);

        return 1;
}

static void ppptp_up(void *data, int arg)
{
	char buf[INET_ADDRES_LEN];
	const char *reason = "connect";
        DBusMessageIter iter, dict;
	DBusMessage *msg;
	char *resolved_server_address;

        syslog(LOG_INFO, "%s: %s: interface up %s", syslog_prefix,
			__func__, ifname);

	if (connection == NULL) {
		syslog(LOG_ERR, "%s: %s: connection not set",
				syslog_prefix, __func__);
		return;
	}

	if (ipcp_gotoptions[0].ouraddr == 0) {
		syslog(LOG_ERR, "%s: %s: our address not set",
				syslog_prefix, __func__);
		return;
	}

	msg = dbus_message_new_method_call(busname, path,
						interface, "notify");
	if (msg == NULL) {
		syslog(LOG_ERR, "%s: %s: unable to create dbus message",
				syslog_prefix, __func__);
		return;
	}

	dbus_message_set_no_reply(msg, TRUE);

	dbus_message_append_args(msg,
			DBUS_TYPE_STRING, &reason, DBUS_TYPE_INVALID);

	dbus_message_iter_init_append(msg, &iter);

	dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY,
			DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
			DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_STRING_AS_STRING
			DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict);

	append(&dict, "INTERNAL_IFNAME", ifname);

	inet_ntop(AF_INET, &ipcp_gotoptions[0].ouraddr, buf, INET_ADDRSTRLEN);
	append(&dict, "INTERNAL_IP4_ADDRESS", buf);

	inet_ntop(AF_INET, &ipcp_hisoptions[0].hisaddr, buf, INET_ADDRSTRLEN);
	append(&dict, "EXTERNAL_IP4_ADDRESS", buf);

	if (ipcp_gotoptions[0].default_route) {
		inet_ntop(AF_INET, &ipcp_hisoptions[0].hisaddr, buf,
			INET_ADDRSTRLEN);
		append(&dict, "GATEWAY_ADDRESS", buf);
	}

        if (ipcp_gotoptions[0].dnsaddr[0] || ipcp_gotoptions[0].dnsaddr[1]) {
                if (ipcp_gotoptions[0].dnsaddr[0]) {
			inet_ntop(AF_INET, &ipcp_gotoptions[0].dnsaddr[0],
							buf, INET_ADDRSTRLEN);
                        append(&dict, "DNS1", buf);
		}
                if (ipcp_gotoptions[0].dnsaddr[1]) {
			inet_ntop(AF_INET, &ipcp_gotoptions[0].dnsaddr[1],
							buf, INET_ADDRSTRLEN);
                        append(&dict, "DNS2", buf);
		}
        }

	resolved_server_address = getenv("LNS_ADDRESS");
	if (resolved_server_address != NULL)
		append(&dict, "LNS_ADDRESS", resolved_server_address);

	dbus_message_iter_close_container(&iter, &dict);

	dbus_connection_send(connection, msg, NULL);

	dbus_connection_flush(connection);

	dbus_message_unref(msg);
}

static void pptp_exit(void *data, int arg)
{
	if (connection != NULL) {
		dbus_connection_unref(connection);
		connection = NULL;
	}

	if (busname != NULL) {
		free(busname);
		busname = NULL;
	}

	if (interface != NULL) {
		free(interface);
		interface = NULL;
	}

	if (path != NULL) {
		free(path);
		path = NULL;
	}
}

static void pptp_phase_change(void *data, int arg)
{
        const char *reason = "disconnect";
	DBusMessage *msg;

        syslog(LOG_INFO, "%s: %s: Change to %s (%d)", syslog_prefix,
			__func__, get_phase_name(arg), arg);

	if (connection == NULL) {
		syslog(LOG_ERR, "%s: %s: connection not set",
				syslog_prefix, __func__);
		return;
	}

	if (arg == PHASE_DEAD || arg == PHASE_DISCONNECT) {
		msg = dbus_message_new_method_call(busname, path,
						interface, "notify");
		if (msg == NULL) {
			syslog(LOG_ERR, "%s: %s: unable to create dbus "
					"call", syslog_prefix, __func__);
			return;
		}

		dbus_message_set_no_reply(msg, TRUE);

		dbus_message_append_args(msg,
			DBUS_TYPE_STRING, &reason, DBUS_TYPE_INVALID);

		dbus_connection_send(connection, msg, NULL);

		dbus_connection_flush(connection);

		dbus_message_unref(msg);
	}
}

int plugin_init(void)
{
	DBusError error;
	static const char *bus, *inter, *p;

	dbus_error_init(&error);

	bus = getenv("CONNMAN_BUSNAME");
	inter = getenv("CONNMAN_INTERFACE");
	p = getenv("CONNMAN_PATH");

	if (!bus || !inter || !p) {
		syslog(LOG_ERR, "%s: %s: bus, interface, or path not set",
				syslog_prefix, __func__);
		return -1;
	}

	busname = strdup(bus);
	interface = strdup(inter);
	path = strdup(p);

	if (!busname || !interface || !path) {
		syslog(LOG_ERR, "%s: %s: alloc of bus, interface, or path "
                                "failed", syslog_prefix, __func__);
		pptp_exit(NULL, 0);
		return -1;
	}

	connection = dbus_bus_get(DBUS_BUS_SYSTEM, &error);
	if (connection == NULL) {
		syslog(LOG_ERR, "%s: %s: connection not set",
				syslog_prefix, __func__);
		if (dbus_error_is_set(&error) == TRUE)
                        dbus_error_free(&error);

		pptp_exit(NULL, 0);
		return -1;
	}

	pap_passwd_hook = pptp_get_secret;
	chap_passwd_hook = pptp_get_secret;

	chap_check_hook = pptp_have_secret;
	pap_check_hook = pptp_have_secret;

	add_notifier(&ip_up_notifier, ppptp_up, NULL);
	add_notifier(&phasechange, pptp_phase_change, NULL);
	add_notifier(&exitnotify, pptp_exit, connection);

	return 0;
}
